/*
 * Copyright (C) 2023 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.squareup.wire.java

import com.palantir.javapoet.JavaFile
import com.squareup.wire.schema.Extend
import com.squareup.wire.schema.Field
import com.squareup.wire.schema.Location
import com.squareup.wire.schema.Schema
import com.squareup.wire.schema.SchemaHandler
import com.squareup.wire.schema.Service
import com.squareup.wire.schema.Type
import java.io.IOException
import okio.Path

class JavaSchemaHandler(
  /** True for emitted types to implement `android.os.Parcelable`. */
  private val android: Boolean = false,

  /** True to enable the `androidx.annotation.Nullable` annotation where applicable. */
  private val androidAnnotations: Boolean = false,

  /**
   * True to emit code that uses reflection for reading, writing, and toString methods which are
   * normally implemented with generated code.
   */
  private val compact: Boolean = false,

  /** True to emit types for options declared on messages, fields, etc. */
  private val emitDeclaredOptions: Boolean = true,

  /** True to emit annotations for options applied on messages, fields, etc. */
  private val emitAppliedOptions: Boolean = true,

  /** If true, the constructor of all generated types will be non-public. */
  private val buildersOnly: Boolean = false,
) : SchemaHandler() {
  private lateinit var javaGenerator: JavaGenerator

  override fun handle(schema: Schema, context: Context) {
    val profileName = if (android) "android" else "java"
    val profile = context.profileLoader!!.loadProfile(profileName, schema)
    javaGenerator = JavaGenerator.get(schema)
      .withProfile(profile)
      .withAndroid(android)
      .withAndroidAnnotations(androidAnnotations)
      .withCompact(compact)
      .withOptions(emitDeclaredOptions, emitAppliedOptions)
      .withBuildersOnly(buildersOnly)

    context.fileSystem.createDirectories(context.outDirectory)

    super.handle(schema, context)
  }

  override fun handle(type: Type, context: Context): Path? {
    if (JavaGenerator.builtInType(type.type)) return null

    val typeSpec = javaGenerator.generateType(type)
    val javaTypeName = javaGenerator.generatedTypeName(type)
    return write(javaTypeName, typeSpec, type.type, type.location, context)
  }

  override fun handle(service: Service, context: Context): List<Path> {
    // Service handling isn't supporting in Java.
    return emptyList()
  }

  override fun handle(extend: Extend, field: Field, context: Context): Path? {
    val typeSpec = javaGenerator.generateOptionType(extend, field) ?: return null
    val javaTypeName = javaGenerator.generatedTypeName(extend.member(field))
    return write(javaTypeName, typeSpec, field.qualifiedName, field.location, context)
  }

  private fun write(
    javaTypeName: com.palantir.javapoet.ClassName,
    typeSpec: com.palantir.javapoet.TypeSpec,
    source: Any,
    location: Location,
    context: Context,
  ): Path {
    val outDirectory = context.outDirectory
    val javaFile = JavaFile.builder(javaTypeName.packageName(), typeSpec)
      .addFileComment("\$L", CODE_GENERATED_BY_WIRE)
      .addFileComment("\nSource: \$L in \$L", source, location.withPathOnly())
      .build()
    val filePath = outDirectory /
      javaFile.packageName().replace(".", "/") /
      "${javaTypeName.simpleName()}.java"

    context.logger.artifactHandled(
      outDirectory,
      "${javaFile.packageName()}.${javaFile.typeSpec().name()}",
      "Java",
    )
    try {
      context.fileSystem.createDirectories(filePath.parent!!)
      context.fileSystem.write(filePath) {
        writeUtf8(javaFile.toString())
      }
    } catch (e: IOException) {
      throw IOException(
        "Error emitting ${javaFile.packageName()}.${javaFile.typeSpec().name()} to $outDirectory",
        e,
      )
    }
    return filePath
  }

  companion object {
    private const val CODE_GENERATED_BY_WIRE = "Code generated by Wire protocol buffer compiler, do not edit."
  }
}
